// TODO: CODE REVIEW & CLEANUP!
//
// MusicServer.cs
//
// Implements MusicServer and related types.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;
using AxWMPLib;
using WMPLib;
using System.Xml;
using System.Text.RegularExpressions;
using System.Globalization;

namespace HomeUX.MusicServer
{

// represents the single music server state that's shared among all clients
public class MusicServer
{
    const string QueueName = "HomeUX Music Server Queue";

    // statics
    static MusicServer s_musicServer;

    static MyComparers _myComparers = new MyComparers();

    List<MusicWcfService> _clients = new List<MusicWcfService>();

    // call this method to invoke the method supplied as the first argument on the
    // application thread
    Action<Action> _beginInvoke;

    AxWindowsMediaPlayer _mediaPlayer;
    IWMPMediaCollection2 _library;

    // key is artist name
    Dictionary<string, Artist> _artists;

    // key is album ID
    Dictionary<string, Album> _albums;

    // artists, sorted by Artist.SortKey
    Artist[] _sortedArtists;

    public int ArtistCount
    {
        get
        {
            return _artists.Count;
        }
    }

    public int AlbumCount
    {
        get
        {
            return _albums.Count;
        }
    }

    EnsureSingleThreaded _ensureSingleThreaded = new EnsureSingleThreaded();

    int _albumArtistAtom;
    int _albumTitleAtom;
    int _albumIdAtom;
    int _trackNumberAtom;
    int _durationAtom;
    int _composerAtom;
    int _yearAtom;
    int _trackUrlAtom;
    int _mediaTypeAtom;

    TrackId _currentTrackId = null;
    int _currentTrackQueueIndex = -1; // 0-based index of currently track within queue
    MusicServicePlayState _currentPlayState;
    double _currentPosition; // current position within current track, in seconds

    /// <summary>
    /// The number of clients currently connected to <r>MusicServer</r>.
    /// </summary>
    public int ClientCount
    {
        get
        {
            return _clients.Count;
        }
    }

    /// <summary>
    /// Fired when <r>ClientCount</r> changes.
    /// </summary>
    public event EventHandler ClientCountChange;

    // will be null if there is no current track
    public TrackId CurrentTrackId
    {
        get
        {
            return _currentTrackId;
        }
    }

    public event EventHandler CurrentTrackIdChange;

	// -1 if there's no current track, or the current track of the internal media player isn't
	// one that's in the queue
	public int CurrentTrackQueueIndex
	{
		get
		{
			return _currentTrackQueueIndex;
		}
	}

    public event EventHandler CurrentTrackQueueIndexChange;

    public MusicServicePlayState CurrentPlayState
    {
        get
        {
            return _currentPlayState;
        }
    }

    public event EventHandler CurrentStatusChange;

    public double CurrentPosition
    {
        get
        {
            return _currentPosition;
        }
    }

    public event EventHandler CurrentPositionChange;

    public event EventHandler<QueueChangeEventArgs> QueueChange;

    /// <summary>
    /// Initializes an instance of this class. 
    /// </summary>
    ///
    /// <param name="mediaPlayer">The Windows Media Player ActiveX control to use.</param>
    ///
    /// <param name="beginInvoke">This is a method that can be called by
    ///     <r>MusicServer</r> to invoke the method supplied as the first argument on the
    ///     application thread, asynchronously.</param>
    ///
    public MusicServer(AxWindowsMediaPlayer mediaPlayer, Action<Action> beginInvoke)
    {
        s_musicServer = this;
        _mediaPlayer = mediaPlayer;
        _beginInvoke = beginInvoke;
    }

    // call after hooking up events
    public void Initialize()
    {
        try
        {
            // set volume to almost its maximum -- note that this is the Media Player volume, not the
            // system volume
            _mediaPlayer.settings.volume = 90;

            _mediaPlayer.CurrentItemChange += MediaPlayer_CurrentItemChange;
            _mediaPlayer.PlayStateChange += MediaPlayer_PlayStateChange;
            _mediaPlayer.PositionChange += _mediaPlayer_PositionChange;
            _mediaPlayer.MediaCollectionChange += MediaPlayer_MediaCollectionChange;
            _mediaPlayer.PlaylistChange += mediaPlayer_PlaylistChange;

            _library = (IWMPMediaCollection2)_mediaPlayer.mediaCollection;
            _albumArtistAtom = _library.getMediaAtom("WM/AlbumArtist");
            _albumTitleAtom = _library.getMediaAtom("WM/AlbumTitle");
            _albumIdAtom = _library.getMediaAtom("AlbumIDAlbumArtist");
            _trackNumberAtom = _library.getMediaAtom("WM/TrackNumber");
            _durationAtom = _library.getMediaAtom("Duration");
            _composerAtom = _library.getMediaAtom("WM/Composer");
            _yearAtom = _library.getMediaAtom("WM/Year");
			_trackUrlAtom = _library.getMediaAtom("SourceURL");
			_mediaTypeAtom = _library.getMediaAtom("MediaType");

            EnumerateLibrary();

            // initialize player state
            OnQueueChange();
            CheckForStatusChange(false);

            // initialize the queue (_mediaPlayer.currentPlaylist)
            IWMPPlaylist queue;
            IWMPPlaylistArray playlists = _mediaPlayer.playlistCollection.getByName(QueueName);
            if (playlists.count > 0)
                queue = playlists.Item(0);
            else
                queue = _mediaPlayer.playlistCollection.newPlaylist(QueueName);
            _mediaPlayer.currentPlaylist = queue;
            _mediaPlayer.Ctlcontrols.stop();

            // begin accepting connections from clients
            var serviceHost = new ServiceHost(typeof(MusicWcfService));
            serviceHost.Open();
        }
        catch (AddressAlreadyInUseException ex)
        {
            throw new MusicServerException(ex,
                "This application is already running, or its port is reserved by another application");
        }
    }

    public static MusicServer OnClientConnected(MusicWcfService wcfService)
    {
        s_musicServer._ensureSingleThreaded.Verify();
        s_musicServer._clients.Add(wcfService);
        if (s_musicServer.ClientCountChange != null)
            s_musicServer.ClientCountChange(s_musicServer, EventArgs.Empty);
        return s_musicServer;
    }

    public void OnClientDisconnected(MusicWcfService wcfService)
    {
        _beginInvoke(delegate
        {
            s_musicServer._ensureSingleThreaded.Verify();
            s_musicServer._clients.Remove(wcfService);
            if (ClientCountChange != null)
                ClientCountChange(this, EventArgs.Empty);
        });
    }

    /// <summary>
    /// The application should call this method approximately twice per second to allow
    /// <r>MediaPlayer</r> to check the current status of the media being played.
    /// </summary>
    ///
    public void Poll()
    {
        _ensureSingleThreaded.Verify();
        CheckForStatusChange(true);
    }

    public ArtistInfo[] GetArtists()
    {
        _ensureSingleThreaded.Verify();

        var artistInfos = new ArtistInfo[_artists.Count];
        int artistIndex = 0;
        foreach (Artist artist in _artists.Values)
        {
            var artistInfo = new ArtistInfo()
            {
                Name = artist.Name,
                SortKey = artist.SortKey
            };
            artistInfos[artistIndex++] = artistInfo;
        }

        return artistInfos;
    }

    public AlbumInfo[] GetAlbumsOfArtist(string artistName, GetAlbumsOfArtistOptions options)
    {
        _ensureSingleThreaded.Verify();

        Artist artist = GetArtist(artistName);

        var albums = artist.Albums.Values;
        AlbumInfo[] albumInfos = new AlbumInfo[albums.Count];
        int albumIndex = 0;
        bool getAlbumArt = (options & GetAlbumsOfArtistOptions.GetAlbumArt) != 0;
        foreach (Album album in albums)
        {
            albumInfos[albumIndex++] = new AlbumInfo()
            {
                Id = album.Id,
                Name = album.Name,
                AlbumArt = getAlbumArt ? GetAlbumArt(album) : null
            };
        }

        return albumInfos;
    }

    byte[] GetAlbumArt(Album album)
    {
        if (Directory.Exists(album.Url))
        {
            string albumArtFileName = Path.Combine(album.Url, "Folder.jpg");
            if (File.Exists(albumArtFileName))
                return File.ReadAllBytes(albumArtFileName);
            else
                return null;
        }
        else
            return null;
    }

    public AlbumDetails GetAlbumDetails(string albumId)
    {
        _ensureSingleThreaded.Verify();

        Album album = GetAlbum(albumId);

        byte[] albumArtBytes = GetAlbumArt(album);

        IWMPPlaylist rawTracks = GetRawTrackList(albumId);
        int rawTrackCount = rawTracks.count;

        var tracks = new Dictionary<int, TrackDetails>(rawTrackCount);

        for (int rawTrackIndex = 0; rawTrackIndex < rawTrackCount; rawTrackIndex++)
        {
            IWMPMedia track = rawTracks.get_Item(rawTrackIndex);

            TrackDetails trackDetails = GetTrackDetails(track);

            // add <trackDetails> to <track>, unless there's already a track with that same track
            // number (e.g. due to ripping in multiple locations, or copy/paste of a track file,
            // etc.)
            TrackDetails otherTrack;
            if (!tracks.TryGetValue(trackDetails.TrackNumber, out otherTrack))
                tracks.Add(trackDetails.TrackNumber, trackDetails);

            //#warning for debugging...
            //StringBuilder sb = new StringBuilder();
            //for (int i = 0; i < track.attributeCount; i++)
            //    sb.AppendFormat("{0}={1}\r\n", track.getAttributeName(i),
            //        track.getItemInfo(track.getAttributeName(i)));
        }

        var albumDetails = new AlbumDetails()
        {
            Name = album.Name,
            ArtistName = album.Artist.Name,
            AlbumArt = albumArtBytes,
            Tracks = tracks.Values.ToArray()
        };

        return albumDetails;
    }


    IWMPPlaylist Query(string propertyValue, params string[] propertyNames)
    {
        IWMPQuery query = _library.createQuery();
        bool isFirst = true;
        foreach (string propertyName in propertyNames)
        {
            if (isFirst)
                isFirst = false;
            else
                query.beginNextGroup();
            query.addCondition(propertyName, "Contains", propertyValue);
        }
        return _library.getPlaylistByQuery(query, "audio", String.Empty, false);
    }

    /// <summary>
    /// Returns <n>true</n> if two strings are equal, using a case-insensitive comparison.
    /// </summary>
    ///
    /// <param name="x">The first string to compare.</param>
    ///
    /// <param name="y">The second string to compare.</param>
    ///
    bool EqualsCi(string x, string y)
    {
        return String.Equals(x, y,StringComparison.CurrentCultureIgnoreCase);
    }

    /// <summary>
    /// Returns <n>true</n> if one string contains another, using a case-insensitive comparison.
    /// </summary>
    ///
    /// <param name="x">The first string.</param>
    ///
    /// <param name="y">The second string.</param>
    ///
    bool ContainsCi(string x, string y)
    {
        return x.IndexOf(y, StringComparison.CurrentCultureIgnoreCase) >= 0;
    }

    /// <summary>
    /// Moves all elements of an array that match either of a given pair of conditions to the
    /// beginning of the returned array.  Elements matching the first condition precede elements
    /// matching the second condition.
    /// </summary>
    ///
    /// <typeparam name="T">The type of elements of the array.</typeparam>
    ///
    /// <param name="array">The array.</param>
    ///
    /// <param name="condition1">The first condition.</param>
    ///
    /// <param name="condition2">The second condition.</param>
    ///
    /// <returns>
    /// The resulting new array.
    /// </returns>
    ///
    T[] Prioritize<T>(T[] array, Func<T, bool> condition1, Func<T, bool> condition2)
    {
        List<T> first = new List<T>(array.Length);
        List<T> middle = new List<T>(array.Length);
        List<T> last = new List<T>(array.Length);
        foreach (T item in array)
        {
            if (condition1(item))
                first.Add(item);
            else
            if (condition2(item))
                middle.Add(item);
            else
                last.Add(item);
        }

        // optimization: if all elements were added to <first> or <middle> or <last>, then the
        // original array can be used as-is
        if ((first.Count == array.Length) || (middle.Count == array.Length) ||
            (last.Count == array.Length))
            return array;

        // return an array consisting of <first> followed by <middle> followed by <last>
        first.AddRange(middle);
        first.AddRange(last);
        return first.ToArray();
    }

    public SearchResult Search(SearchRequest request)
    {
		// search for artists matching <request.Find>
        Dictionary<string, Artist> foundArtists;
        if (request.MaxArtists >= 0)
        {
            foundArtists = new Dictionary<string, Artist>();
            IWMPPlaylist tracks = Query(request.Find,
                "WM/AlbumArtist", "WM/Composer");
            for (int trackIndex = 0; trackIndex < tracks.count; trackIndex++)
            {
                IWMPMedia track = tracks.get_Item(trackIndex);
                string artistName = Clean(track.getItemInfoByAtom(_albumArtistAtom));
                Artist artist;
                if (_artists.TryGetValue(artistName, out artist))
                    foundArtists[artistName] = artist;
            }
        }
        else
            foundArtists = null;

		// search for albums matching <request.Find>
        Dictionary<string, Album> foundAlbums;
        if (request.MaxAlbums >= 0)
        {
            foundAlbums = new Dictionary<string, Album>();
            IWMPPlaylist tracks = Query(request.Find,
                "WM/AlbumTitle", "WM/AlbumArtist", "WM/Composer");
            for (int trackIndex = 0; trackIndex < tracks.count; trackIndex++)
            {
                IWMPMedia track = tracks.get_Item(trackIndex);
                string albumId = Clean(track.getItemInfoByAtom(_albumIdAtom));

                Album album;
                if (_albums.TryGetValue(albumId, out album))
                    foundAlbums[albumId] = album;
            }
        }
        else
            foundAlbums = null;

		// search for tracks matching <request.Find>
        Dictionary<FoundTrack, IWMPMedia> foundTracks;
        if (request.MaxTracks >= 0)
        {
            foundTracks = new Dictionary<FoundTrack, IWMPMedia>(_myComparers);
            IWMPPlaylist tracks = Query(request.Find,
                "Title", "WM/AlbumArtist", "WM/Composer");
            for (int trackIndex = 0; trackIndex < tracks.count; trackIndex++)
            {
                IWMPMedia track = tracks.get_Item(trackIndex);
                var foundTrack = new FoundTrack()
                {
                    TrackId = GetTrackIdFromTrack(track)
                };
                foundTracks[foundTrack] = track;
            }
        }
        else
            foundTracks = null;

        var result = new SearchResult()
        {
            FoundArtistCount = (foundArtists != null) ? foundArtists.Count : -1,
            FoundAlbumCount = (foundAlbums != null) ? foundAlbums.Count : -1,
            FoundTrackCount = (foundTracks != null) ? foundTracks.Count : -1
        };

        if (foundArtists != null)
        {
            result.FoundArtists = new FoundArtist[
                Math.Min(foundArtists.Count, request.MaxArtists)];
            int index = 0;
            foreach (string artistName in foundArtists.Keys)
            {
                if (index >= request.MaxArtists)
                    break;
                result.FoundArtists[index++] = new FoundArtist()
                {
                    ArtistName = artistName
                };
            }

            // rearrange <result.FoundArtists> so that exact matches to <request.Find> come first,
            // followed by substring matches, followed by other matches (all case-insensitive)
            result.FoundArtists = Prioritize(result.FoundArtists,
                foundArtist => EqualsCi(foundArtist.ArtistName, request.Find),
                foundArtist => ContainsCi(foundArtist.ArtistName, request.Find));
        }

        if (foundAlbums != null)
        {
            result.FoundAlbums = new FoundAlbum[
                Math.Min(foundAlbums.Count, request.MaxAlbums)];
            int index = 0;
            foreach (Album album in foundAlbums.Values)
            {
                if (index >= request.MaxAlbums)
                    break;

				byte[] albumArtBytes;
				if (request.GetAlbumArt)
					albumArtBytes = GetAlbumArt(album);
				else
					albumArtBytes = null;

                result.FoundAlbums[index++] = new FoundAlbum()
                {
                    AlbumId = album.Id,
					ArtistName = album.Artist.Name,
					AlbumName = album.Name,
					AlbumArt = albumArtBytes
                };
            }

            // rearrange <result.FoundAlbums> so that exact matches to <request.Find> come first,
            // followed by substring matches, followed by other matches (all case-insensitive)
            result.FoundAlbums = Prioritize(result.FoundAlbums,
                foundAlbum => EqualsCi(foundAlbum.AlbumName, request.Find),
                foundAlbum => ContainsCi(foundAlbum.AlbumName, request.Find));
        }

        if (foundTracks != null)
        {
            result.FoundTracks = new FoundTrack[
                Math.Min(foundTracks.Count, request.MaxTracks)];
            int index = 0;
            foreach (KeyValuePair<FoundTrack, IWMPMedia> kvp in foundTracks)
            {
                if (index >= request.MaxTracks)
                    break;
                FoundTrack foundTrack = kvp.Key;
                foundTrack.ArtistName = Clean(kvp.Value.getItemInfoByAtom(_albumArtistAtom));
                foundTrack.AlbumName = Clean(kvp.Value.getItemInfoByAtom(_albumTitleAtom));
                foundTrack.TrackName = Clean(kvp.Value.name);
                result.FoundTracks[index++] = foundTrack;
            }

            // rearrange <result.FoundTracks> so that exact matches to <request.Find> come first,
            // followed by substring matches, followed by other matches (all case-insensitive)
            result.FoundTracks = Prioritize(result.FoundTracks,
                foundTrack => EqualsCi(foundTrack.TrackName, request.Find),
                foundTrack => ContainsCi(foundTrack.TrackName, request.Find));
        }

        /* alternative:
        IWMPStringCollection2 titles = (WMPLib.IWMPStringCollection2)
            _library.getStringCollectionByQuery("Title", query, "audio", "", false);
        */

        return result;
    }

    public QueueDetails GetQueueDetails()
    {
        _ensureSingleThreaded.Verify();

        IWMPPlaylist queue = GetQueue();
        int queueLength = queue.count;

        var queueDetails = new QueueDetails()
        {
            Tracks = new AlbumTrackDetails[queueLength]
        };

        for (int queueIndex = 0; queueIndex < queueLength; queueIndex++)
        {
            IWMPMedia track = queue.get_Item(queueIndex);

            AlbumTrackDetails albumTrackDetails = new AlbumTrackDetails()
            {
                ArtistName = Clean(track.getItemInfoByAtom(_albumArtistAtom)),
                AlbumId = Clean(track.getItemInfoByAtom(_albumIdAtom)),
                AlbumName = Clean(track.getItemInfoByAtom(_albumTitleAtom)),
                TrackDetails = GetTrackDetails(track)
            };

            queueDetails.Tracks[queueIndex] = albumTrackDetails;
        }

        return queueDetails;
    }

    int GetQueueIndexOfTrack(IWMPPlaylist queue, TrackId trackId)
    {
        IWMPMedia trackInQueue;
        return GetQueueIndexOfTrack(queue, trackId, out trackInQueue);
    }

    /// <summary>
    /// Returns the zero-based index of a given track within the queue.  Returns -1 if the track
    /// is not present in the queue.
    /// </summary>
    /// <param name="queue"></param>
    /// <param name="trackId"></param>
    /// <returns></returns>
    int GetQueueIndexOfTrack(IWMPPlaylist queue, TrackId trackId, out IWMPMedia trackInQueue)
    {
        int queueLength = queue.count;
        for (int queueIndex = 0; queueIndex < queueLength; queueIndex++)
        {
            trackInQueue = queue.get_Item(queueIndex);
            TrackId trackInQueueId = GetTrackIdFromTrack(trackInQueue);
            if (MusicServerHelper.AreTracksEqual(trackId, trackInQueueId))
                return queueIndex;
        }

        trackInQueue = null;
        return -1;
    }

    // clear the queue and play the specified album, starting at the specified track;
    // all tracks of the album are in the queue so that "|<" and ">|" navigate among all
    // tracks; if there's no track numbered <startAtTrackNumber> (e.g. <startAtTrackNumber is -1),
    // play begins at the first track
    public void PlayAlbum(string albumId, int startAtTrackNumber)
    {
        _ensureSingleThreaded.Verify();

        IWMPPlaylist rawTracks = GetRawTrackList(albumId);
        int rawTrackCount = rawTracks.count;

        var tracks = new Dictionary<int, TrackDetails>(rawTrackCount);

        _currentTrackQueueIndex = 0;
        IWMPPlaylist queue = GetQueue();
        queue.clear();
		IWMPMedia startAtTrack = null;
        int lowestTrackNumber = int.MaxValue;
        IWMPMedia firstTrack = null; // the track numbered <lowestTrackNumber>
        for (int rawTrackIndex = 0; rawTrackIndex < rawTrackCount; rawTrackIndex++)
        {
            IWMPMedia track = rawTracks.get_Item(rawTrackIndex);

			queue.appendItem(track);

            int trackNumber = GetTrackNumberFromTrack(track);

            if (lowestTrackNumber > trackNumber)
            {
                lowestTrackNumber = trackNumber;
                firstTrack = track;
            }

			if (trackNumber == startAtTrackNumber)
				startAtTrack = track;
		}

        // start the playlist playing, starting at <startAtTrack>
        //_mediaPlayer.currentPlaylist = queue;
        if (startAtTrack != null)
            _mediaPlayer.Ctlcontrols.playItem(startAtTrack);
        else
        if (firstTrack != null)
            _mediaPlayer.Ctlcontrols.playItem(firstTrack);
    }

    public void AddToQueue(TrackId[] trackIds)
    {
        _ensureSingleThreaded.Verify();
        IWMPPlaylist queue = GetQueue();

        foreach (TrackId trackId in trackIds)
        {
            IWMPMedia track = FindTrackByNumber(trackId.AlbumId, trackId.TrackNumber);

            // if the track is currently playing, do nothing; if it's elsewhere in the queue,
            // move it to the end; we do this in a loop for code robustness reasons (i.e. in case
            // the queue somehow ended up with a duplicate of this track)
            while (true)
            {
                int queueIndex = GetQueueIndexOfTrack(queue, GetTrackIdFromTrack(track));
                if (queueIndex < 0)
                    break;
                else
                    if (queueIndex == _currentTrackQueueIndex)
                        return;
                    else
                        queue.removeItem(track);
            }

            queue.appendItem(track);
        }
    }

    public void RemoveFromQueue(TrackId[] trackIds)
    {
        IWMPPlaylist queue = GetQueue();
        var tracksToDelete = new List<IWMPMedia>();
        foreach (TrackId trackId in trackIds)
        {
            IWMPMedia trackInQueue;
            GetQueueIndexOfTrack(queue, trackId, out trackInQueue);
            if (trackInQueue != null)
                tracksToDelete.Add(trackInQueue);
        }
        foreach (IWMPMedia track in tracksToDelete)
            queue.removeItem(track);
    }

    IWMPMedia GetTrackFromTrackId(TrackId trackId)
    {
        IWMPQuery query = _library.createQuery();
        query.addCondition("AlbumIDAlbumArtist", "Equals", trackId.AlbumId);
        query.addCondition("WM/TrackNumber", "Equals", XmlConvert.ToString(trackId.TrackNumber));
        IWMPPlaylist tracks = _library.getPlaylistByQuery(query, "audio", String.Empty, false);
        if (tracks.count == 0)
        {
            throw NewMusicServiceFaultException("Album \"{0}:{1}\" not found", trackId.AlbumId,
                trackId.TrackNumber);
        }
        return tracks.get_Item(0);
    }

    public void ChangeQueue(TrackId[] trackIds)
    {
        // set <tracks> to the IWMPMedia array equivalent of <trackIds>
        int index = 0;
        var tracks = new IWMPMedia[trackIds.Length];
        foreach (TrackId trackId in trackIds)
            tracks[index++] = GetTrackFromTrackId(trackId);

        // set the queue to be <tracks>
        IWMPPlaylist queue = GetQueue();
        queue.clear();
        foreach (IWMPMedia track in tracks)
            queue.appendItem(track);
    }

    public void ClearQueue()
    {
        IWMPPlaylist queue = GetQueue();
        queue.clear();
    }

    public PlaylistInfo[] GetPlaylists()
    {
        IWMPPlaylistArray playlists = _mediaPlayer.playlistCollection.getAll();
        int playlistCount = playlists.count;
        var playlistInfos = new List<PlaylistInfo>(playlistCount);
        for (int playlistIndex = 0; playlistIndex < playlistCount; playlistIndex++)
        {
            IWMPPlaylist playlist = playlists.Item(playlistIndex);

            if (playlist.name != QueueName)
            {
                string playlistType = playlist.getItemInfo("PlaylistType");
                if (playlistType == "wpl")
                {
                    playlistInfos.Add(new PlaylistInfo()
                    {
                        Name = Clean(playlist.name)
                    });
                }
            }

            //#warning for debugging...
            //StringBuilder sb = new StringBuilder();
            //for (int i = 0; i < playlist.attributeCount; i++)
            //    sb.AppendFormat("{0}={1}\r\n", playlist.get_attributeName(i),
            //        playlist.getItemInfo(playlist.get_attributeName(i)));
        }

        return playlistInfos.ToArray();
    }

    public void LoadPlaylist(string playlistName)
    {
		// set <playlist> to the playlist named <playlistName>
        IWMPPlaylist playlist = GetPlaylist(playlistName);

		// load <playlist> into the queue; remove duplicates in the process
        IWMPPlaylist queue = GetQueue();
        queue.clear();
        int trackCount = playlist.count;
        var trackIdToIndex = new Dictionary<TrackId, int>(trackCount,
            MusicServerHelper.TrackIdEqualityComparer);
        for (int trackIndex = 0; trackIndex < trackCount; trackIndex++)
        {
            IWMPMedia track = playlist.get_Item(trackIndex);
            TrackId trackId = GetTrackIdFromTrack(track);
            int unused;
            if (!trackIdToIndex.TryGetValue(trackId, out unused))
            {
                trackIdToIndex.Add(trackId, trackIndex);
                queue.appendItem(track);
            }
        }
    }

    public void SaveQueue(string playlistName)
    {
        // set <playlist> to the playlist named <playlistName>, which is created if it doesn't
        // exist
        IWMPPlaylistArray playlists = _mediaPlayer.playlistCollection.getByName(playlistName);
        IWMPPlaylist playlist;
        if (playlists.count == 0)
            playlist = _mediaPlayer.playlistCollection.newPlaylist(playlistName);
        else
        if (playlists.count == 1)
            playlist = playlists.Item(0);
        else
        {
            throw NewMusicServiceFaultException("More than one playlist named \"{0}\" was found",
                playlistName);
        }

        // save the queue to <playlist>
        IWMPPlaylist queue = GetQueue();
        playlist.clear();
        int trackCount = queue.count;
        for (int trackIndex = 0; trackIndex < trackCount; trackIndex++)
        {
            IWMPMedia track = queue.get_Item(trackIndex);
            playlist.appendItem(track);
        }
    }

    public void DeletePlaylist(string playlistName)
    {
		// set <playlist> to the playlist named <playlistName>
        IWMPPlaylist playlist = GetPlaylist(playlistName);

        // delete the playlist
        _mediaPlayer.playlistCollection.remove(playlist);
    }

    IWMPPlaylist GetPlaylist(string playlistName)
    {
        IWMPPlaylistArray playlists = _mediaPlayer.playlistCollection.getByName(playlistName);
        if (playlists.count == 0)
            throw NewMusicServiceFaultException("Playlist \"{0}\" not found", playlistName);
        else
        if (playlists.count > 1)
        {
            throw NewMusicServiceFaultException("More than one playlist named \"{0}\" was found",
                playlistName);
        }

        return playlists.Item(0);
    }

    public void Play()
    {
        _mediaPlayer.Ctlcontrols.play();
    }

    public void Pause()
    {
        _mediaPlayer.Ctlcontrols.pause();
    }

    public void Stop()
    {
        _mediaPlayer.Ctlcontrols.stop();
    }

    public void FastForward()
    {
        _mediaPlayer.Ctlcontrols.fastForward();
    }

    public void FastReverse()
    {
        bool isFeatureAvailable = _mediaPlayer.Ctlcontrols.get_isAvailable("FastReverse");
        if (isFeatureAvailable)
            _mediaPlayer.Ctlcontrols.fastReverse();
        else
        {
            // the FastReverse feature isn't available -- just go back a bit
            _mediaPlayer.Ctlcontrols.currentPosition -= 20;
        }
    }

    public void NextTrack()
    {
        _mediaPlayer.Ctlcontrols.next();
    }

    public void PreviousTrack()
    {
        _mediaPlayer.Ctlcontrols.previous();
    }

    public void PlayQueueItem(TrackId trackId)
    {
        IWMPPlaylist queue = GetQueue();
		int queueIndex = GetQueueIndexOfTrack(queue, trackId);
		if (queueIndex >= 0)
			_mediaPlayer.Ctlcontrols.playItem(queue.get_Item(queueIndex));
    }

    public MusicServiceDetailedStatus GetDetailedStatus()
    {
        var status = new MusicServiceDetailedStatus();
        
        GetStatus(status);

        if (status.AlbumId != null) // i.e. if there is a current album
        {
            Album album = GetAlbum(status.AlbumId);

            AlbumDetails albumDetails = GetAlbumDetails(status.AlbumId);

            status.ArtistName = albumDetails.ArtistName;
            status.AlbumName = albumDetails.Name;
            status.AlbumArt = albumDetails.AlbumArt;

            TrackDetails track = FindTrack(albumDetails.Tracks, status.TrackNumber);
            if (track != null)
            {
                status.TrackName = track.Name;
                status.TrackDuration = track.Duration;
                status.TrackYear = track.Year;
                status.TrackComposer = track.Composer;
            }

            QueueDetails queueDetails = GetQueueDetails();
            bool foundTrackInQueue = false;
            for (int queueIndex = 0; queueIndex < queueDetails.Tracks.Length; queueIndex++)
            {
                AlbumTrackDetails trackInQueue = queueDetails.Tracks[queueIndex];
                if ((trackInQueue.AlbumId == status.AlbumId) &&
                    (trackInQueue.TrackDetails.TrackNumber == status.TrackNumber))
                {
                    status.QueueIndex = queueIndex; // trust this over what GetStatus returns
                    foundTrackInQueue = true;
                }
                else
                if (foundTrackInQueue)
                {
                    status.QueuedTrackCount++;
                    status.QueuedTrackDuration += trackInQueue.TrackDetails.Duration;
                }
            }
        }

        status.Settings = GetSettings();

        return status;
    }

    public TrackDetails FindTrack(TrackDetails[] tracks, int trackNumber)
    {
        foreach (TrackDetails track in tracks)
        {
            if (track.TrackNumber == trackNumber)
                return track;
        }
        return null;
    }

    //public AlbumTrackDetails FindTrackInQueue(string albumId, int trackNumber)
    //{
    //    QueueDetails queueDetails = GetQueueDetails();
    //    for (int queueIndex = 0; queueIndex < queueDetails.Tracks.Length; queueIndex++)
    //    {
    //        AlbumTrackDetails trackInQueue = queueDetails.Tracks[queueIndex];
    //        if ((trackInQueue.AlbumId == albumId) &&
    //            (trackInQueue.TrackDetails.TrackNumber == trackNumber))
    //        {
    //            return trackInQueue;
    //        }
    //    }

    //    return null;
    //}

    public MusicServiceSettings GetSettings()
    {
        return new MusicServiceSettings()
        {
            Volume = _mediaPlayer.settings.volume,
            IsMuted = _mediaPlayer.settings.mute,
            PlaybackRate = _mediaPlayer.settings.rate,
            IsLoopMode = _mediaPlayer.settings.getMode("loop"),
            IsShuffleMode = _mediaPlayer.settings.getMode("shuffle")
        };
    }

    public void SetSettings(MusicServiceSettings settings)
    {
        // <settings> may include partial settings (i.e. some fields nulled); make
        // <fullSettings> contain all settings
        MusicServiceSettings fullSettings = GetSettings();

        if (settings.Volume.HasValue)
            _mediaPlayer.settings.volume = (fullSettings.Volume = settings.Volume).Value;
        if (settings.IsMuted.HasValue)
			_mediaPlayer.settings.mute = (fullSettings.IsMuted = settings.IsMuted).Value;
        if (settings.PlaybackRate.HasValue)
			_mediaPlayer.settings.rate = (fullSettings.PlaybackRate = settings.PlaybackRate).Value;
        if (settings.IsLoopMode.HasValue)
			_mediaPlayer.settings.setMode("loop", (fullSettings.IsLoopMode = settings.IsLoopMode).Value);
        if (settings.IsShuffleMode.HasValue)
			_mediaPlayer.settings.setMode("shuffle", (fullSettings.IsShuffleMode = settings.IsShuffleMode).Value);

        // report <fullSettings> to all clients
        foreach (MusicWcfService client in _clients)
            client.ReportSettingsChange(fullSettings);
    }

    // internal:

    Artist GetArtist(string artistName)
    {
        try
        {
            return _artists[artistName];
        }
        catch (KeyNotFoundException)
        {
            // NOTE: "FaultException`1 was unhandled" happens if you run in Visual Studio IDE;
            // just press F5
            throw NewMusicServiceFaultException("Artist \"{0}\" not found", artistName);
        }
    }

    Album GetAlbum(string albumId)
    {
        try
        {
            return _albums[albumId];
        }
        catch (KeyNotFoundException)
        {
            // NOTE: "FaultException`1 was unhandled" happens if you run in Visual Studio IDE;
            // just press F5
            throw NewMusicServiceFaultException("Album \"{0}\" not found", albumId);
        }
    }

    //TrackId GetTrackId(IWMPMedia track)
    //{
    //    if (track == null)
    //        return null;
    //    var trackId = new TrackId();
    //    trackId.AlbumId = Clean(track.getItemInfoByAtom(_albumIdAtom));
    //    int.TryParse(track.getItemInfoByAtom(_trackNumberAtom),
    //        out trackId.TrackNumber);
    //    return trackId;
    //}

    TrackDetails GetTrackDetails(IWMPMedia track)
    {
        var trackDetails = new TrackDetails()
        {
            Name = Clean(track.name),
            TrackNumber = GetTrackNumberFromTrack(track)
        };
        double.TryParse(track.getItemInfoByAtom(_durationAtom),
            out trackDetails.Duration);
        trackDetails.Composer = Clean(track.getItemInfoByAtom(_composerAtom));
        int.TryParse(track.getItemInfoByAtom(_yearAtom),
            out trackDetails.Year);

        return trackDetails;
    }

    IWMPPlaylist GetQueue()
    {
        return _mediaPlayer.currentPlaylist;
    }

    //IWMPPlaylist GetQueue()
    //{
    //    IWMPPlaylist queue = _mediaPlayer.currentPlaylist;
    //    if (queue != null)
    //        return queue;

    //    IWMPPlaylistArray playlists = _mediaPlayer.playlistCollection.getByName(QueueName);
    //    if (playlists.count > 0)
    //        queue = playlists.Item(0);
    //    else
    //        queue = _mediaPlayer.playlistCollection.newPlaylist(QueueName);
    //    _mediaPlayer.currentPlaylist = queue;
    //    _mediaPlayer.Ctlcontrols.stop();
    //    return queue;
    //}

    /// <summary>
    /// Returns the "raw" track list for an album, i.e. the list of tracks that may include
    /// duplicates (i.e. tracks with the same track number).
    /// </summary>
    ///
    /// <param name="albumId">The ID of the album to look for.</param>
    ///
    /// <remarks>
    /// Duplicate tracks can occur because an album is ripped in multiple disk locations, or a
    /// track file is copied and pasted with a different name in the same directory, or various
    /// other reasons.
    /// </remarks>
    ///
    IWMPPlaylist GetRawTrackList(string albumId)
    {
        return _library.getByAttribute("AlbumIDAlbumArtist", albumId);
    }

    IWMPMedia FindTrackByNumber(string albumId, int trackNumber)
    {
        IWMPPlaylist rawTracks = GetRawTrackList(albumId);
        int rawTrackCount = rawTracks.count;

        for (int rawTrackIndex = 0; rawTrackIndex < rawTrackCount; rawTrackIndex++)
        {
            IWMPMedia track = rawTracks.get_Item(rawTrackIndex);
            int thisTrackNumber = GetTrackNumberFromTrack(track);

            if (thisTrackNumber == trackNumber)
                return track;
        }

        // NOTE: "FaultException`1 was unhandled" happens if you run in Visual Studio IDE;
        // just press F5
        throw NewMusicServiceFaultException("Track {0} of album \"{1}\" not found",
            trackNumber, albumId);
    }

    FaultException NewMusicServiceFaultException(string format, params object[] args)
    {
        var fault = new MusicServiceFault()
        {
            Message = Program.Format(format, args)
        };

        return new FaultException<MusicServiceFault>(fault,
            new FaultReason(fault.Message));
    }

    public void EnumerateLibrary()
    {
        Dictionary<string, Artist> artists = new Dictionary<string, Artist>();
        Dictionary<string, Album> albums = new Dictionary<string, Album>();

        StringBuilder warnings = new StringBuilder(1000);

        // if <fastEnumeration> is true, enumeration is much faster, but misses albums that
        // don't have a track #1
        bool fastEnumeration = true;
        IWMPPlaylist foundTracks;
        if (fastEnumeration)
            foundTracks = _library.getByAttributeAndMediaType("WM/TrackNumber", "1", "audio");
        else
            foundTracks = _library.getAll();

        int foundTrackCount = foundTracks.count;
        for (int foundTrackIndex = 0; foundTrackIndex < foundTrackCount; foundTrackIndex++)
        {
            IWMPMedia foundTrack = foundTracks.get_Item(foundTrackIndex);

            // skip <foundTrack> if it's not actually an audio track
            string mediaType = foundTrack.getItemInfoByAtom(_mediaTypeAtom);
            if (mediaType != "audio")
                continue;

            string artistName = Clean(foundTrack.getItemInfoByAtom(_albumArtistAtom));
            string albumName = Clean(foundTrack.getItemInfoByAtom(_albumTitleAtom));
            string albumId = Clean(foundTrack.getItemInfoByAtom(_albumIdAtom));
            string trackUrl = Clean(foundTrack.getItemInfoByAtom(_trackUrlAtom));
			int trackNumber;
            if ((artistName.Length == 0) || (albumName.Length == 0) || (albumId.Length == 0) ||
			    ((trackNumber = GetTrackNumberFromTrack(foundTrack)) == 0))
            {
                // ignore this track -- it probably isn't from a ripped album
                continue;
            }

            string albumUrl;
            try
            {
                albumUrl = Path.GetDirectoryName(trackUrl);
            }
            catch (Exception ex)
            {
                if ((ex is ArgumentException) || (ex is PathTooLongException))
                    albumUrl = trackUrl;
                else
                    throw;
            }

            Artist artist;
            if (!artists.TryGetValue(artistName, out artist))
            {
                artist = new Artist()
                {
                    MusicServer = this,
                    Name = artistName,
                    SortKey = Program.GetSortKey(artistName)
                };
                artists.Add(artistName, artist);
            }

            Album otherAlbum;
            if (!albums.TryGetValue(albumId, out otherAlbum))
            {
                // <albumId> is an album we haven't seen yet
                var album = new Album()
                {
                    Artist = artist,
                    Id = albumId,
                    Name = albumName,
                    Url = albumUrl
                };
                albums.Add(albumId, album);
                artist.Albums.Add(albumId, album);
            }
            else
            if (albumUrl != otherAlbum.Url)
            {
                // album <albumId> has files in two locations
                warnings.AppendFormat(
                    "Duplicate album \"{0}\" (ID \"{1}\", locations \"{2}\" and \"{3}\")\n",
                    albumName, albumId, albumUrl, otherAlbum.Url);
            }

            //#warning for debugging: set <sb> to properties of <firstTrack>
            //StringBuilder sb = new StringBuilder();
            //for (int i = 0; i < foundTrack.attributeCount; i++)
            //    sb.AppendFormat("{0}={1}\r\n", foundTrack.getAttributeName(i),
            //        foundTrack.getItemInfo(foundTrack.getAttributeName(i)));
        }

        if (warnings.Length > 0)
        {
            Program.ReportWarningOnlyInWindowsService(
                "Problem(s) encountered while enumerating music library:\r\n\r\n{0}",
                warnings);
        }

        _sortedArtists = artists.Values.ToArray();
        Array.Sort(_sortedArtists, (x, y) => String.CompareOrdinal(x.SortKey, y.SortKey));

        _artists = artists;
        _albums = albums;

        foreach (MusicWcfService client in _clients)
            client.ReportLibraryChange();
    }

    /// <summary>
    /// Checks to see if a status change event needs to be sent to clients.
    /// </summary>
    ///
    /// <param name="onlyCheckPosition">If <n>true</n>, only check to see if the current media
    ///     position has changed since the last call to <r>CheckForStatusChange</r>.  If
    ///     <n>false</n>, check everything that needs to be monitored.</param>
    ///
    void CheckForStatusChange(bool onlyCheckPosition)
    {
        bool changeOccurred = false;

        if (!onlyCheckPosition)
        {
            // update <_currentTrackId> as needed (note that a null value means "no current
            // track", e.g. the queue is empty); also update <_currentTrackQueueIndex> as needed
            TrackId trackId = GetTrackIdFromTrack(_mediaPlayer.currentMedia);
            if (((_currentTrackId == null) != (trackId == null)) ||
                ((_currentTrackId != null) && (trackId != null) &&
                 (((_currentTrackId.AlbumId != trackId.AlbumId) ||
                  (_currentTrackId.TrackNumber != trackId.TrackNumber)))))
            {
                // the current track changed
                _currentTrackId = trackId;
                changeOccurred = true;

                // tell the application UI
                if (CurrentTrackIdChange != null)
                    CurrentTrackIdChange(this, EventArgs.Empty);

                // update <_currentTrackQueueIndex> as needed
                if (CheckCurrentTrackQueueIndex())
                    changeOccurred = true;
            }

            // update <_currentPlayState> as needed
            if (trackId != null)
            {
                WMPPlayState mediaPlayerPlayState = _mediaPlayer.playState;
                MusicServicePlayState playState;
                switch (mediaPlayerPlayState)
                {
                    case WMPPlayState.wmppsPlaying:
                        playState = MusicServicePlayState.Playing;
                        break;
                    case WMPPlayState.wmppsScanForward:
                        playState = MusicServicePlayState.ScanForward;
                        break;
                    case WMPPlayState.wmppsScanReverse:
                        playState = MusicServicePlayState.ScanReverse;
                        break;
                    case WMPPlayState.wmppsPaused:
                        playState = MusicServicePlayState.Paused;
                        break;
                    case WMPPlayState.wmppsStopped:
                    case WMPPlayState.wmppsMediaEnded:
                    case WMPPlayState.wmppsUndefined:
                    case WMPPlayState.wmppsReady:
                        playState = MusicServicePlayState.Stopped;
                        break;
                    default:
                        playState = (MusicServicePlayState)(-1);
                        break;
                }
                if (playState != (MusicServicePlayState)(-1))
                {
                    if (_currentPlayState != playState)
                    {
                        // current status changed
                        _currentPlayState = playState;
                        changeOccurred = true;
                        if (CurrentStatusChange != null)
                            CurrentStatusChange(this, EventArgs.Empty);
                    }
                }
            }
            else
            {
                // there's no "current track" -- e.g. the queue is empty
                if (_currentPlayState != MusicServicePlayState.NoCurrentTrack)
                {
                    _currentPlayState = MusicServicePlayState.NoCurrentTrack;
                    changeOccurred = true;
                    if (CurrentStatusChange != null)
                        CurrentStatusChange(this, EventArgs.Empty);
                }
            }
        }

        // update <_currentPosition> as needed
        if ((_currentPlayState == MusicServicePlayState.Playing) ||
            (_currentPlayState == MusicServicePlayState.Paused) ||
            (_currentPlayState == MusicServicePlayState.ScanForward) ||
            (_currentPlayState == MusicServicePlayState.ScanReverse) ||
            ((_currentPlayState == MusicServicePlayState.Stopped) && (_currentPosition != 0)))
        {
            double currentPosition = _mediaPlayer.Ctlcontrols.currentPosition;
            if (Math.Abs(_currentPosition - currentPosition) > 0.1)
            {
                // current position changed
                _currentPosition = currentPosition;
                changeOccurred = true;
                if (CurrentPositionChange != null)
                    CurrentPositionChange(this, EventArgs.Empty);
            }
        }

        if (changeOccurred)
        {
            var status = new MusicServiceStatus();
            GetStatus(status);
            foreach (MusicWcfService client in _clients)
                client.ReportStatusChange(status);
        }
    }

    /// <summary>
    /// Checks to see if <r>_currentTrackQueueIndex</r> is correct; if so, it returns <n>false</n>;
    /// if not, it corrects it, fires a <r>CurrentTrackQueueIndexChange</r> event, and returns
    /// <n>true</n> (indicating it was changed).
    /// </summary>
    bool CheckCurrentTrackQueueIndex()
    {
        IWMPPlaylist queue = GetQueue();
        int queueIndex = GetQueueIndexOfTrack(queue, _currentTrackId);
        if (_currentTrackQueueIndex != queueIndex)
        {
            _currentTrackQueueIndex = queueIndex;
            if (CurrentTrackQueueIndexChange != null)
                CurrentTrackQueueIndexChange(this, EventArgs.Empty);
            return true;
        }

        return false;
    }

    void GetStatus(MusicServiceStatus status)
    {
        if (_currentTrackId != null)
        {
            status.PlayState = _currentPlayState;
            status.AlbumId = _currentTrackId.AlbumId;
            status.TrackNumber = _currentTrackId.TrackNumber;
            status.Position = _currentPosition;
            status.QueueIndex = _currentTrackQueueIndex;
        }
        else
        {
            // there's no current track
            status.PlayState = MusicServicePlayState.NoCurrentTrack;
            status.AlbumId = null;
            status.TrackNumber = 0;
            status.Position = 0;
            status.QueueIndex = -1;
        }
    }

    // called when a different track becomes the current track in MediaPlayer
    private void MediaPlayer_CurrentItemChange(object sender, _WMPOCXEvents_CurrentItemChangeEvent e)
    {
        _ensureSingleThreaded.Verify();
        CheckForStatusChange(false);
    }

    // called when MediaPlayer changes state (stopped, paused, playing, etc.)
    private void MediaPlayer_PlayStateChange(object sender, _WMPOCXEvents_PlayStateChangeEvent e)
    {
        _ensureSingleThreaded.Verify();
        CheckForStatusChange(false);
    }

    // called when the position is changed while paused or playing, not including position changes
    // due to normal progression of time during play, fast forward, etc.
    void _mediaPlayer_PositionChange(object sender, _WMPOCXEvents_PositionChangeEvent e)
    {
        _ensureSingleThreaded.Verify();
        CheckForStatusChange(true);
    }

    // called (I think) when content is added to, or removed from, the library
    void MediaPlayer_MediaCollectionChange(object sender, EventArgs e)
    {
        _ensureSingleThreaded.Verify();
        EnumerateLibrary();
    }

    // called when any playlist changes
    void mediaPlayer_PlaylistChange(object sender, _WMPOCXEvents_PlaylistChangeEvent e)
    {
        _ensureSingleThreaded.Verify();

        // the wmplcPrivate event happens asynchronously after all the rest -- which causes
        // duplicate queue-changed events, and it's not supported anyway, so we'll ignore it
        if (e.change == WMPPlaylistChangeEventType.wmplcPrivate)
            return;

        var playlist = e.playlist as IWMPPlaylist;
        if ((playlist != null) && (playlist.name == QueueName))
        {
            if (!_onQueueChangePending)
            {
                _onQueueChangePending = true;
                _beginInvoke(OnQueueChange);
            }
        }
    }

    bool _onQueueChangePending;

    void OnQueueChange()
    {
        _ensureSingleThreaded.Verify();

        _onQueueChangePending = false;

        CheckForStatusChange(false);

        CheckCurrentTrackQueueIndex();

        QueueDetails queueDetails = GetQueueDetails();
        if (QueueChange != null)
        {
            QueueChange(this, new QueueChangeEventArgs()
            {
                QueueDetails = queueDetails
            });
        }
        foreach (MusicWcfService client in _clients)
            client.ReportQueueChange(queueDetails);
    }

    static readonly Regex BadCharsRegex = new Regex("\x0000");
    /// <summary>
    /// Cleans up a string by removing characters that can't be transmitted via XML.
    /// </summary>
    static string Clean(string str)
    {
        return BadCharsRegex.Replace(str, String.Empty);
    }

	string GetAlbumIdFromTrack(IWMPMedia track)
	{
        if (track == null)
            return null;
		return Clean(track.getItemInfoByAtom(_albumIdAtom));
	}

	int GetTrackNumberFromTrack(IWMPMedia track)
	{
        if (track == null)
            return 0;
		string valueAsString = track.getItemInfoByAtom(_trackNumberAtom);
		if (String.IsNullOrEmpty(valueAsString))
			return 0;
		try
		{
			return XmlConvert.ToInt32(valueAsString);
		}
		catch (Exception ex)
		{
			if ((ex is FormatException) || (ex is OverflowException))
				return 0;
			else
				throw;
		}
	}

	TrackId GetTrackIdFromTrack(IWMPMedia track)
	{
        if (track == null)
            return null;
		return new TrackId()
		{
			AlbumId = GetAlbumIdFromTrack(track),
			TrackNumber = GetTrackNumberFromTrack(track)
		};
	}
}

class MyComparers : IEqualityComparer<FoundTrack>
{
    bool IEqualityComparer<FoundTrack>.Equals(FoundTrack x, FoundTrack y)
    {
        return (x.TrackId.AlbumId == y.TrackId.AlbumId) &&
               (x.TrackId.TrackNumber == y.TrackId.TrackNumber);
    }

    int IEqualityComparer<FoundTrack>.GetHashCode(FoundTrack x)
    {
        return x.TrackId.AlbumId.GetHashCode() ^ x.TrackId.TrackNumber.GetHashCode();
    }
}

[DebuggerDisplay("Artist {Name}")]
public class Artist
{
    public MusicServer MusicServer;
    public string Name;
    public string SortKey;
    public Dictionary<String, Album> Albums = new Dictionary<string, Album>();
}

[DebuggerDisplay("Album {Name}")]
public class Album
{
    public Artist Artist;
    public string Id;
    public string Name;
    public string Url;
}

public class QueueChangeEventArgs : EventArgs
{
    public QueueDetails QueueDetails { get; set; }
}

public class MusicServerException : Exception
{
    public MusicServerException(Exception innerException, string format, params object[] args) :
        base(Program.Format(format, args), innerException)
    {
    }
}

public class EnsureSingleThreaded
{
#if DEBUG
    int _creationThreadId = Thread.CurrentThread.ManagedThreadId;
    string _creationThreadName = Thread.CurrentThread.Name ?? "unnamed";
#endif

    [Conditional("DEBUG")]
    public void Verify()
    {
#if DEBUG
        int currentThreadId = Thread.CurrentThread.ManagedThreadId;
        if (currentThreadId != _creationThreadId)
        {
            string currentThreadName = Thread.CurrentThread.Name ?? "unnamed";
            throw new InvalidOperationException(String.Format(
                "EnsureSingleThreaded.Verify failed: object was created on thread {0} ({1}) but is now running on thread {2} ({3})",
                _creationThreadId, _creationThreadName, currentThreadId, currentThreadName));
        }
#endif
    }
}

}
